LEÇON 8

Tests et Débogage en Python

Tests unitaires, assertions, débogueur pdb, logging et bonnes pratiques

Français - Tests et Débogage

Pourquoi tester son code ? Les tests permettent de vérifier que votre code fonctionne comme prévu, d'éviter les régressions et de faciliter la maintenance. Un code testé est un code fiable.

1. L'instruction assert

assert est une assertion qui vérifie qu'une condition est vraie. Si elle est fausse, une exception AssertionError est levée.

# Syntaxe de base
assert condition, "message d'erreur optionnel"

# Exemples
x = 10
assert x > 0, "x doit être positif" # OK
assert x == 5, "x devrait être 5" # Lève AssertionError

# Utilisation dans une fonction
def diviser(a, b):
    assert b != 0, "Division par zéro impossible"
    return a / b
Attention : Les assertions peuvent être désactivées avec l'option -O de Python. Ne les utilisez pas pour la validation de données critiques en production.

2. Tests Unitaires avec unittest

Le module unittest est intégré à Python et permet d'écrire des tests structurés.

# Fonction à tester
def additionner(a, b):
    return a + b

def est_pair(n):
    return n % 2 == 0

# Classe de tests (fichier test_calculs.py)
import unittest

class TestCalculs(unittest.TestCase):
    
    def test_additionner(self):
        self.assertEqual(additionner(2, 3), 5)
        self.assertEqual(additionner(-1, 1), 0)
        self.assertEqual(additionner(0, 0), 0)

    def test_est_pair(self):
        self.assertTrue(est_pair(4))
        self.assertFalse(est_pair(5))
        self.assertTrue(est_pair(0))

    def test_additionner_negatifs(self):
        self.assertLess(additionner(-5, -3), 0)

if __name__ == '__main__':
    unittest.main()
# Méthodes d'assertion courantes dans unittest
# assertEqual(a, b) - a == b
# assertNotEqual(a, b) - a != b
# assertTrue(x) - bool(x) is True
# assertFalse(x) - bool(x) is False
# assertIs(a, b) - a is b
# assertIsNone(x) - x is None
# assertIn(a, b) - a in b
# assertRaises(Error) - vérifie qu'une exception est levée

3. Tests avec pytest

pytest est une alternative plus simple et plus puissante à unittest. (Nécessite pip install pytest)

# Fichier: test_calculs_pytest.py
# Avec pytest, pas besoin de classe, juste des fonctions

def additionner(a, b):
    return a + b

def diviser(a, b):
    return a / b

# Tests simples
def test_additionner():
    assert additionner(2, 3) == 5
    assert additionner(-1, 1) == 0
    assert additionner(0, 0) == 0

def test_diviser_par_zero():
    import pytest
    with pytest.raises(ZeroDivisionError):
        diviser(10, 0)

# Paramétrage de tests
import pytest

@pytest.mark.parametrize("a,b,attendu", [
    (1, 1, 2),
    (2, 3, 5),
    (-1, 5, 4),
])
def test_additionner_param(a, b, attendu):
    assert additionner(a, b) == attendu
Exécution des tests :
python -m unittest discover pour unittest
pytest ou pytest -v pour pytest (plus lisible)

4. Débogage avec pdb

pdb est le débogueur interactif intégré de Python.

Commandes pdb essentielles :
l (list) - Affiche le code autour de la ligne actuelle
n (next) - Exécute la ligne suivante
s (step) - Entre dans une fonction
c (continue) - Continue l'exécution
p variable (print) - Affiche la valeur d'une variable
q (quit) - Quitte le débogueur
b 10 (break) - Place un point d'arrêt à la ligne 10
# Méthode 1: Ajouter dans le code
import pdb

def fonction_problematique(x, y):
    resultat = x + y
    pdb.set_trace() # Le programme s'arrête ici
    return resultat * 2

# Méthode 2 (Python 3.7+): breakpoint()
def autre_fonction(valeur):
    breakpoint() # Équivalent plus moderne
    return valeur ** 2
# Exemple complet avec pdb
def calculer_moyenne(notes):
    total = 0
    for note in notes:
        total += note
    breakpoint() # Vérifions total et len(notes)
    return total / len(notes)

notes = [15, 12, 18, 14]
moyenne = calculer_moyenne(notes)
print(moyenne)

5. Journalisation (logging)

Le module logging est plus professionnel que print() pour le suivi et le débogage.

import logging

# Configuration de base
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("app.log"), # Écrit dans un fichier
        logging.StreamHandler() # Affiche dans la console
    ]
)

# Niveaux de log (par ordre croissant de gravité)
logging.debug("Message de débogage - détails techniques")
logging.info("Message d'information - fonctionnement normal")
logging.warning("Message d'avertissement - problème potentiel")
logging.error("Message d'erreur - problème mais programme continue")
logging.critical("Message critique - programme va s'arrêter")
# Exemple d'utilisation dans une application
import logging

logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')

class GestionnaireUtilisateur:
    def __init__(self):
        self.utilisateurs = {}
        logging.info("GestionnaireUtilisateur initialisé")

    def ajouter(self, nom, age):
        if nom in self.utilisateurs:
            logging.warning(f"Tentative d'ajout d'un utilisateur existant: {nom}")
            return False
        self.utilisateurs[nom] = age
        logging.debug(f"Utilisateur ajouté: {nom}, {age} ans")
        return True

    def obtenir(self, nom):
        try:
            return self.utilisateurs[nom]
        except KeyError:
            logging.error(f"Utilisateur non trouvé: {nom}")
            return None

6. Bonnes pratiques de débogage

Conseils essentiels :
  • ✔️ Lisez attentivement les messages d'erreur et les tracebacks
  • ✔️ Isolez le problème (réduisez le code au minimum reproduisant le bug)
  • ✔️ Utilisez print() stratégiquement, mais préférez logging pour les projets sérieux
  • ✔️ Testez vos hypothèses une par une
  • ✔️ Écrivez des tests avant de corriger un bug (TDD - Test Driven Development)
  • ✔️ Utilisez un IDE avec débogueur intégré (VSCode, PyCharm)
# Utilisation de __debug__ pour du code conditionnel
if __debug__:
    print("Ce message n'apparaît qu'en mode debug")
    logging.debug("Mode debug actif")

# Lancer Python sans mode debug: python -O script.py
# __debug__ devient False
Attention : Ne laissez jamais de pdb.set_trace() ou de breakpoint() en production. Utilisez des variables d'environnement ou des flags pour activer conditionnellement le débogage.

English - Testing and Debugging

Why test your code? Tests verify that your code works as expected, prevent regressions, and make maintenance easier. Tested code is reliable code.

1. The assert Statement

assert checks that a condition is true. If false, it raises an AssertionError exception.

# Basic syntax
assert condition, "optional error message"

# Examples
x = 10
assert x > 0, "x must be positive" # OK
assert x == 5, "x should be 5" # Raises AssertionError

# Usage in a function
def divide(a, b):
    assert b != 0, "Cannot divide by zero"
    return a / b
Warning: Assertions can be disabled with Python's -O option. Don't use them for critical data validation in production.

2. Unit Testing with unittest

The unittest module is built into Python and allows writing structured tests.

# Function to test
def add(a, b):
    return a + b

def is_even(n):
    return n % 2 == 0

# Test class (file test_calculations.py)
import unittest

class TestCalculations(unittest.TestCase):
    
    def test_add(self):
        self.assertEqual(add(2, 3), 5)
        self.assertEqual(add(-1, 1), 0)
        self.assertEqual(add(0, 0), 0)

    def test_is_even(self):
        self.assertTrue(is_even(4))
        self.assertFalse(is_even(5))
        self.assertTrue(is_even(0))

if __name__ == '__main__':
    unittest.main()

3. Testing with pytest

pytest is a simpler and more powerful alternative to unittest. (Requires pip install pytest)

# File: test_calculations_pytest.py
# With pytest, no class needed, just functions

def add(a, b):
    return a + b

def divide(a, b):
    return a / b

# Simple tests
def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0
    assert add(0, 0) == 0

def test_divide_by_zero():
    import pytest
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

# Test parametrization
import pytest

@pytest.mark.parametrize("a,b,expected", [
    (1, 1, 2),
    (2, 3, 5),
    (-1, 5, 4),
])
def test_add_param(a, b, expected):
    assert add(a, b) == expected

4. Debugging with pdb

pdb is Python's built-in interactive debugger.

Essential pdb commands:
l (list) - Show code around current line
n (next) - Execute next line
s (step) - Step into a function
c (continue) - Continue execution
p variable (print) - Print variable value
q (quit) - Quit debugger
b 10 (break) - Set breakpoint at line 10
# Method 1: Add in code
import pdb

def problematic_function(x, y):
    result = x + y
    pdb.set_trace() # Program stops here
    return result * 2

# Method 2 (Python 3.7+): breakpoint()
def another_function(value):
    breakpoint() # More modern equivalent
    return value ** 2

5. Logging

The logging module is more professional than print() for monitoring and debugging.

import logging

# Basic configuration
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("app.log"), # Writes to file
        logging.StreamHandler() # Prints to console
    ]
)

# Log levels (increasing severity)
logging.debug("Debug message - technical details")
logging.info("Info message - normal operation")
logging.warning("Warning message - potential issue")
logging.error("Error message - problem but program continues")
logging.critical("Critical message - program will stop")
Essential debugging tips:
  • ✔️ Read error messages and tracebacks carefully
  • ✔️ Isolate the problem (reduce code to minimal reproduction)
  • ✔️ Use print() strategically, but prefer logging for serious projects
  • ✔️ Test your hypotheses one by one
  • ✔️ Write tests before fixing bugs (TDD - Test Driven Development)
  • ✔️ Use an IDE with integrated debugger (VSCode, PyCharm)